OperatorSQLBuilderFactory.java
package org.codefilarete.stalactite.query.builder;
import javax.annotation.Nullable;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import org.codefilarete.stalactite.query.builder.FunctionSQLBuilderFactory.FunctionSQLBuilder;
import org.codefilarete.stalactite.query.model.ConditionalOperator;
import org.codefilarete.stalactite.query.model.Fromable;
import org.codefilarete.stalactite.query.model.Selectable;
import org.codefilarete.stalactite.query.model.ValuedVariable;
import org.codefilarete.stalactite.query.model.Variable;
import org.codefilarete.stalactite.query.model.operator.Between;
import org.codefilarete.stalactite.query.model.operator.Between.Interval;
import org.codefilarete.stalactite.query.model.operator.Equals;
import org.codefilarete.stalactite.query.model.operator.Greater;
import org.codefilarete.stalactite.query.model.operator.In;
import org.codefilarete.stalactite.query.model.operator.IsNull;
import org.codefilarete.stalactite.query.model.operator.Lesser;
import org.codefilarete.stalactite.query.model.operator.Like;
import org.codefilarete.stalactite.query.model.operator.SQLFunction;
import org.codefilarete.stalactite.query.model.operator.TupleIn;
import org.codefilarete.stalactite.sql.ddl.structure.Column;
import org.codefilarete.tool.Reflections;
import org.codefilarete.tool.VisibleForTesting;
/**
* A class made to print a {@link ConditionalOperator}
*
* @author Guillaume Mary
*/
public class OperatorSQLBuilderFactory {
public OperatorSQLBuilderFactory() {
}
public OperatorSQLBuilder operatorSQLBuilder(FunctionSQLBuilder functionSQLBuilder) {
return new OperatorSQLBuilder(functionSQLBuilder);
}
public static class OperatorSQLBuilder {
private final FunctionSQLBuilder functionSQLBuilder;
public OperatorSQLBuilder(FunctionSQLBuilder functionSQLBuilder) {
this.functionSQLBuilder = functionSQLBuilder;
}
/**
* Main entry point
*/
public void cat(ConditionalOperator operator, SQLAppender sql) {
if (operator instanceof TupleIn) {
catTupledIn((TupleIn) operator, sql);
} else {
cat(null, operator, sql);
}
}
public <V> void cat(Selectable<V> column, ConditionalOperator<?, V> operator, SQLAppender sql) {
if (operator.isNull()) {
catNullValue(operator.isNot(), sql);
} else {
// ugly way of dispatching concatenation, can't find a better way without heaving classes or struggling with single responsibility design
if (operator instanceof Equals) {
catEquals((Equals<V>) operator, sql, column);
} else if (operator instanceof Lesser) {
catLower((Lesser<V>) operator, sql, column);
} else if (operator instanceof Greater) {
catGreater((Greater<V>) operator, sql, column);
} else if (operator instanceof Between) {
catBetween((Between<V>) operator, sql, column);
} else if (operator instanceof In) {
catIn((In<V>) operator, sql, column);
} else if (operator instanceof Like) {
catLike((Like) operator, sql, column);
} else if (operator instanceof IsNull) {
catIsNull((IsNull) operator, sql);
} else {
throw new UnsupportedOperationException("Operator " + Reflections.toString(operator.getClass()) + " is not implemented");
}
}
}
void catNullValue(boolean not, SQLAppender sql) {
// "= NULL" is incorrect and will return no result (answer from Internet) and should be replaced by "is null"
sql.cat("is").catIf(not, " not").cat(" null");
}
void catIsNull(IsNull<?> isNull, SQLAppender sql) {
catNullValue(isNull.isNot(), sql);
}
void catLike(Like like, SQLAppender sql, Selectable<?> column) {
sql.catIf(like.isNot(), "not ").cat("like ");
LikePatternAppender likePatternAppender = new LikePatternAppender(like, sql);
if (like.getValue() instanceof ValuedVariable) {
Object value = ((ValuedVariable<?>) like.getValue()).getValue();
if (value instanceof SQLFunction) {
functionSQLBuilder.cat((SQLFunction) value, likePatternAppender);
} else {
likePatternAppender.catValue(column, like.getValue());
}
} else {
likePatternAppender.catValue(column, like.getValue());
}
}
<V> void catIn(In<V> in, SQLAppender sql, Selectable<V> column) {
// we take collection into account : iterating over it to cat all values
Variable<Iterable<V>> value = in.getValue();
sql.catIf(in.isNot(), "not ").cat("in (");
catInValue(value, sql, column);
sql.cat(")");
}
void catTupledIn(TupleIn in, SQLAppender sql) {
// we take collection into account : iterating over it to cat all values
Column[] columns = in.getColumns();
sql.catIf(in.isNot(), "not ");
sql.cat("(");
Iterator<Column> columnIterator = Arrays.stream(columns).iterator();
while (columnIterator.hasNext()) {
sql.catColumn(columnIterator.next()).catIf(columnIterator.hasNext(), ", ");
}
sql.cat(")");
sql.cat(" in (");
Variable<List<Object[]>> value = in.getValue();
if (value instanceof ValuedVariable<?>) {
List<Object[]> values = ((ValuedVariable<List<Object[]>>) value).getValue();
if (values == null) {
for (int i = 0, columnCount = columns.length; i < columnCount; i++) {
sql.catValue(columns[i], null).catIf(i < columnCount - 1, ", ");
}
} else {
Iterator<Object[]> valuesIterator = values.iterator();
while (valuesIterator.hasNext()) {
Object[] vals = valuesIterator.next();
sql.cat("(");
for (int i = 0, columnCount = columns.length; i < columnCount; i++) {
sql.catValue(columns[i], vals[i]).catIf(i < columnCount - 1, ", ");
}
sql.cat(")").catIf(valuesIterator.hasNext(), ", ");
}
}
}
sql.cat(")");
}
/**
* Appends {@link Iterable} values to the given appender. Essentially done for "in" operator
*
* @param sql the appender
* @param column
* @param value the iterable to be appended, not null (but may be empty, this method doesn't care)
*/
private <V> void catInValue(Variable<Iterable<V>> value, SQLAppender sql, Selectable<V> column) {
// appending values (separated by a comma, boilerplate code)
boolean isFirst = true;
if (value instanceof ValuedVariable) {
for (Object v : ((ValuedVariable<Iterable<V>>) value).getValue()) {
if (!isFirst) {
sql.cat(", ");
} else {
isFirst = false;
}
if (v instanceof SQLFunction) {
functionSQLBuilder.cat((SQLFunction) v, sql);
} else {
sql.catValue(column, v);
}
}
} else {
// we know this cast is wrong : value is Variable<Iterable<V>>. But adding a signature for it is too much work for few usage
sql.catValue(column, value);
}
}
<V> void catBetween(Between<V> between, SQLAppender sql, Selectable<V> column) {
Variable<Interval<V>> value = between.getValue();
if (value instanceof ValuedVariable) {
Interval<V> interval = ((ValuedVariable<Interval<V>>) value).getValue();
if (interval.getValue1() == null) {
sql.cat(between.isNot() ? ">= " : "< ").catValue(column, interval.getValue2());
} else if (interval.getValue2() == null) {
sql.cat(between.isNot() ? "<= " : "> ").catValue(column, interval.getValue1());
} else {
sql.catIf(between.isNot(), "not ").cat("between ")
.catValue(column, interval.getValue1())
.cat(" and ")
.catValue(column, interval.getValue2());
}
}
}
@SuppressWarnings("squid:S3358") // we can afford nesting ternary operators here, not so complex to understand
<V> void catGreater(Greater<V> greater, SQLAppender sql, Selectable<V> column) {
sql.cat(greater.isNot()
? (greater.isEquals() ? "< " : "<= ")
: (greater.isEquals() ? ">= " : "> "));
catValue(column, greater.getValue(), sql);
}
@SuppressWarnings("squid:S3358") // we can afford nesting ternary operators here, not so complex to understand
<V> void catLower(Lesser<V> lesser, SQLAppender sql, Selectable<V> column) {
sql.cat(lesser.isNot()
? (lesser.isEquals() ? "> " : ">= ")
: (lesser.isEquals() ? "<= " : "< "));
catValue(column, lesser.getValue(), sql);
}
<V> void catEquals(Equals<V> equals, SQLAppender sql, Selectable<V> column) {
sql.catIf(equals.isNot(), "!").cat("= ");
catValue(column, equals.getValue(), sql);
}
<V> void catValue(@Nullable Selectable<V> column, Variable<V> variable, SQLAppender sql) {
if (variable instanceof ValuedVariable) {
V value = ((ValuedVariable<V>) variable).getValue();
if (value instanceof SQLFunction) {
functionSQLBuilder.cat((SQLFunction<?, ?>) value, sql);
} else {
sql.catValue(column, variable);
}
} else {
sql.catValue(column, variable);
}
}
/**
* Adds leading and ending "%" while appending Like values.
* Made for cases composed Like with function as argument (lower, upper, ...) because they are not aware of being embedded in a Like operator.
*
* @author Guillaume Mary
*/
@VisibleForTesting
static class LikePatternAppender implements SQLAppender {
private final Like<?> like;
private final SQLAppender sql;
@VisibleForTesting
LikePatternAppender(Like<?> like, SQLAppender sql) {
this.like = like;
this.sql = sql;
}
@Override
public <V> SQLAppender catValue(@Nullable Selectable<V> column, Object variable) {
if (variable instanceof ValuedVariable) {
V value = ((ValuedVariable<V>) variable).getValue();
if (value instanceof CharSequence) {
sql.catValue((Selectable<CharSequence>) column, addWildcards((CharSequence) value));
} else {
sql.catValue(column, variable);
}
} else {
sql.catValue(column, variable);
}
return this;
}
private CharSequence addWildcards(CharSequence effectiveValue) {
if (like.withLeadingStar()) {
effectiveValue = "%" + effectiveValue;
}
if (like.withEndingStar()) {
effectiveValue += "%";
}
return effectiveValue;
}
@Override
public SQLAppender catValue(Object value) {
if (value instanceof ValuedVariable) {
value = ((ValuedVariable<?>) value).getValue();
}
if (value instanceof Selectable) {
// unwrapping raw Selectable
sql.catValue(addWildcards(((Selectable<CharSequence>) value).getExpression()));
} else if (value instanceof CharSequence) {
sql.catValue(addWildcards((CharSequence) value));
} else {
throw new UnsupportedOperationException("Appending '" + value + "'"
+ (value == null ? "" : " (type " + Reflections.toString(value.getClass()) + ")") + " is not supported");
}
return this;
}
@Override
public SQLAppender cat(String s, String... ss) {
sql.cat(s, ss);
return this;
}
@Override
public SQLAppender catColumn(Selectable<?> column) {
sql.catColumn(column);
return this;
}
@Override
public SQLAppender catTable(Fromable table) {
sql.catTable(table);
return this;
}
@Override
public SQLAppender removeLastChars(int length) {
sql.removeLastChars(length);
return this;
}
@Override
public String getSQL() {
return sql.getSQL();
}
@Override
public SubSQLAppender newSubPart(DMLNameProvider dmlNameProvider) {
SQLAppender self = this;
return new DefaultSubSQLAppender(new LikePatternAppender(this.like, this.sql.newSubPart(dmlNameProvider))) {
@Override
public SQLAppender close() {
// nothing special;
return self;
}
};
}
}
}
}